<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>黑客帝国背景</title>
  <style>
      html, body {
          height: 100%;
          margin: 0;
          overflow: hidden;
      }

      body {
          display: flex;
          align-items: center;
          justify-content: center;
          background: #000;
      }

      main {
          display: flex;
      }

      p {
          line-height: 1;
      }

      span {
          display: block;
          width: 2vmax;
          height: 2vmax;
          font-size: 2vmax;
          color: #9bff9b11;
          text-align: center;
          font-family: "Helvetica Neue", Helvetica, sans-serif;
      }
  </style>
</head>
<body>
<main>
</main>
<script>
function r(from, to) {
  return ~~(Math.random() * (to - from + 1) + from);
}

function pick() {
  return arguments[r(0, arguments.length - 1)];
}

function getChar() {
  return String.fromCharCode(pick(
    r(0x3041, 0x30ff),
    r(0x2000, 0x206f),
    r(0x0020, 0x003f)
  ));
}

function loop(fn, delay) {
  let stamp = Date.now();

  function _loop() {
    if (Date.now() - stamp >= delay) {
      fn();
      stamp = Date.now();
    }
    requestAnimationFrame(_loop);
  }

  requestAnimationFrame(_loop);
}

class Char {
  constructor() {
    this.element = document.createElement('span');
    this.mutate();
  }

  mutate() {
    this.element.textContent = getChar();
  }
}

class Trail {
  constructor(list = [], options) {
    this.list = list;
    this.options = Object.assign(
      {size: 10, offset: 0}, options
    );
    this.body = [];
    this.move();
  }

  traverse(fn) {
    this.body.forEach((n, i) => {
      let last = (i == this.body.length - 1);
      if (n) fn(n, i, last);
    });
  }

  move() {
    this.body = [];
    let {offset, size} = this.options;
    for (let i = 0; i < size; ++i) {
      let item = this.list[offset + i - size + 1];
      this.body.push(item);
    }
    this.options.offset =
      (offset + 1) % (this.list.length + size - 1);
  }
}

class Rain {
  constructor({target, row}) {
    this.element = document.createElement('p');
    this.build(row);
    if (target) {
      target.appendChild(this.element);
    }
    this.drop();
  }

  build(row = 20) {
    let root = document.createDocumentFragment();
    let chars = [];
    for (let i = 0; i < row; ++i) {
      let c = new Char();
      root.appendChild(c.element);
      chars.push(c);
      if (Math.random() < .5) {
        loop(() => c.mutate(), r(1e3, 5e3));
      }
    }
    this.trail = new Trail(chars, {
      size: r(10, 30), offset: r(0, 100)
    });
    this.element.appendChild(root);
  }

  drop() {
    let trail = this.trail;
    let len = trail.body.length;
    let delay = r(10, 100);
    loop(() => {
      trail.move();
      trail.traverse((c, i, last) => {
        c.element.style = `
        color: hsl(136, 100%, ${85 / len * (i + 1)}%)
      `;
        if (last) {
          c.mutate();
          c.element.style = `
          color: hsl(136, 100%, 85%);
          text-shadow:
            0 0 .5em #fff,
            0 0 .5em currentColor;
        `;
        }
      });
    }, delay);
  }
}

const main = document.querySelector('main');
for (let i = 0; i < 50; ++i) {
  new Rain({target: main, row: 50});
}
</script>
</body>
</html>
